﻿using System;
using System.Collections.Generic;
using System.Drawing;
using breakoutsharp.sound;
using breakoutsharp.visual.gameobject;

namespace breakoutsharp.core
{
	public class BreakoutEngine : IDisposable
	{
		public enum GameState
		{
			GameNotStarted,
			GameRunning,
			GameOver
		}

		/* Game objects */
		protected Level currentLevel;
		protected List<Paddle> paddles = new List<Paddle>(2);

		/* Sound */
		protected DxSoundPlayer sound = null;
		Microsoft.DirectX.AudioVideoPlayback.Audio backgroundMusic = null;

		/* Game variables */
		protected Size bounds;
		protected GameState state = GameState.GameNotStarted;
		protected const int paddleMoveSpeedPerSecond = 1200;
		protected const bool paddleSideHitEqualsCorner = true;
		protected bool paused = false;
		protected const int gameloopIntervals = 10;
		protected bool playMusic = false;
		protected bool disposedValue = false;


		public BreakoutEngine(IntPtr handle, Size bounds)
		{
			sound = new DxSoundPlayer(handle, @"data\sound\beep.wav");
			backgroundMusic = new Microsoft.DirectX.AudioVideoPlayback.Audio(@"data\sound\protectingmydevotion.mid");

			if (playMusic)
			backgroundMusic.Play();
			this.bounds = bounds;
		}

		public void Update(double timeInMilliSec)
		{
			if (!paused && state == GameState.GameRunning)
			{
				/* breaking down the time elapsed into smaller intervals to make
				 * collision detection deterministic*/
				double timeRemaining = timeInMilliSec;

				while (timeRemaining > gameloopIntervals)
				{
					timeRemaining -= gameloopIntervals;

					/* Move paddles */
					MovePaddles(timeRemaining);

					/* Move balls */
					MoveBalls(timeRemaining);
				}

				// check for game over
				if (IsGameover())
				{
					state = GameState.GameOver;
				}

		
			}
		}

		private void MoveBalls(double timeInMilliSec)
		{
			/* Move all balls */
			foreach (Ball b in currentLevel.Balls)
			{
				/* Only move a ball if it's alive */
				if (b.Alive)
				{
					PointF newLoc = new PointF((float)(b.Location.X + (b.Speed * timeInMilliSec / 1000) * b.Direction.X),
						(float)(b.Location.Y + (b.Speed * timeInMilliSec / 1000) * b.Direction.Y));

					//check for collision
					//if (b.HasBeenCorrolated)
					//{
					//    b.Location = newLoc;

					//    if (!IsColliding(b, newLoc))
					//    {
					//        b.HasBeenCorrolated = false;
					//    }
					//}
					//else
					if (!IsColliding(b, newLoc))
					{
						b.Location = newLoc;
					}
				}
			}
		}

		/// <summary>
		/// Move paddles according to keys pressed. If they collide with balls, then they'll be corolated for
		/// </summary>
		/// <param name="timeInMilliSec"></param>
		private void MovePaddles(double timeInMilliSec)
		{
			/* Move all paddles */
			foreach (Paddle p in paddles)
			{
				/* First calculate the new position of the pad */
				float newPadPosition = p.Location.X;
				float deltaPaddleMovement = 0;


				if (p.Controls.MovingRight)
				{
					newPadPosition = p.Location.X + (float)timeInMilliSec / 1000 * paddleMoveSpeedPerSecond;
					deltaPaddleMovement = (float)timeInMilliSec / 1000 * paddleMoveSpeedPerSecond;

				}
				else if (p.Controls.MovingLeft)
				{
					newPadPosition = p.Location.X - (float)timeInMilliSec / 1000 * paddleMoveSpeedPerSecond;
					deltaPaddleMovement = -(float)timeInMilliSec / 1000 * paddleMoveSpeedPerSecond;
				}

				/* Make sure it doesn't go out of screen */
				if (newPadPosition < 0)
				{
					newPadPosition = 0;
				}
				else if (newPadPosition + p.Size.Width > bounds.Width)
				{
					newPadPosition = bounds.Width - p.Size.Width;
				}

				/* Check if the pad collides with any ball */
				RectangleF padRect = p.GetRectF;

				foreach (Ball b in Balls)
				{
					RectangleF ballRect = b.GetRectF;

					if (ballRect.IntersectsWith(padRect) && !b.HasBeenCorrolated)
					{
						/* If the paddle collides with the ball, then we need to speed up the ball and eventually change direction until ball is
						 * away from the paddle*/

						if (!MovingSameDirection(b.Direction.X, deltaPaddleMovement) || !ObjectMovingUp(b.Direction.Y))
						{
							/* Need to invert ball direction and speed up */
							b.InvertBoth();
						}

						/* Move ball away until it's gone */
						float deltaX = p.Location.X - b.Location.X;
						float deltaY = b.Location.Y - p.Location.Y;

						if (b.Location.X > p.Location.X + p.Size.Width / 2)
						{
							deltaX += p.Size.Width;
						}

						//float xPerSecond = b.Direction.X * b.Speed;
						//float yPerSecond = b.Direction.Y * b.Speed;

						//float needXmovement = deltaX / xPerSecond;
						//float needYmovement = deltaY / yPerSecond;

						//float neededSeconds = Math.Max(needXmovement, needYmovement);

						//b.Location = new PointF(b.Location.X + neededSeconds * b.Direction.X, b.Location.Y + neededSeconds * b.Direction.Y);
						b.Location = new PointF(b.Location.X + deltaX, b.Location.Y + deltaY);
					}
				}

				p.Location = new PointF(newPadPosition, p.Location.Y);
			}
		}

		private bool ObjectMovingUp(float verticalSpeed)
		{
			if (verticalSpeed > 0)
				return true;

			return false;
		}

		private bool MovingSameDirection(float o1, float o2)
		{
			if ((o1 > 0 && o2 > 0) || (o1 < 0 && o2 < 0) || (o1 == 0 && o2 == 0))
				return true;

			return false;

		}

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		protected bool IsGameover()
		{
			bool functionReturnValue = true;
			int belowPaddleAllowance = 20;
			float lowestYCoordPaddle = 0;

			// finding the paddle farest down (this is not necessary if all paddles are on the same y-value!)
			foreach (Paddle p in paddles)
			{
				if (lowestYCoordPaddle < p.Location.Y)
				{
					lowestYCoordPaddle = p.Location.Y;
				}
			}

			// see if any of the balls are below treshold
			foreach (Ball b in currentLevel.Balls)
			{

				if (b.Location.Y < lowestYCoordPaddle + paddles[0].Size.Height + belowPaddleAllowance)
				{
					functionReturnValue = false;
				}
				else
				{
					b.Alive = false;
				}
			}

			return functionReturnValue;
		}

		private bool IsColliding(Ball curBall, PointF newPt)
		{
			bool Colliding = false;
			/* Bounds vs. Ball collision */
			if (newPt.X < 0 || newPt.X >= bounds.Width - currentLevel.Balls[0].Size.Width)
			{
				curBall.InvertHorizontal();
				//Colliding = true;
				return true;
			}

			if (newPt.Y < 0 || newPt.Y >= bounds.Height - currentLevel.Balls[0].Size.Width)
			{
				curBall.InvertVertical();
				//Colliding = true;
				return true;
			}

			/* Ball vs. Paddle detection */
			RectangleF ballRect = new RectangleF(newPt.X, newPt.Y, curBall.Size.Width, curBall.Size.Height);
			foreach (Paddle pad in paddles)
			{
				RectangleF padRect = pad.GetRectF;

				if (padRect.IntersectsWith(ballRect))
				{
					AdvancedRectCollision(curBall, newPt, pad.GetRectF, true);
					//Colliding = true;
					return true;
				}
			}

			//check against blocks
			foreach (IBlock currentBlock in currentLevel.Blocks)
			{
				if (!currentBlock.Dead & currentBlock.Collidable)
				{
					if (currentBlock.GetRectF.IntersectsWith(ballRect))
					{
						AdvancedRectCollision(curBall, newPt, currentBlock.GetRectF, false);

						currentBlock.BallHit(curBall);

						sound.Play();

						//Colliding = true;
						return true;
					}
				}
			}

			return Colliding;
		}

		private void AdvancedRectCollision(Ball b, PointF newPt, RectangleF rect, bool cornerDeflection)
		{
			//if ball is heading upwards, then no collision detection is necessary
			if (b.IsGoingUp && cornerDeflection)
			{
				return;
			}

			//ADVANCED COLLISION DETECTED ALGORITHM: build to give maximum control to the player
			//
			// - if hit corners:
			// * invert x/y direction
			// * increase speed on ball as penalty for more controlah
			// - hit middle:
			// * decrease speed a little bit
			// * deflect ball directly 'up'
			// - hit between corner and middle
			// *


			//The square length of which its defined that the ball hit the cornwer of the paddle
			double cornerLength = rect.Width / 8;

			//The change in y axis for every step in x axis
			double ballGradient = (b.Location.Y - newPt.Y) / (b.Location.X - newPt.X);

			//left corner optimal direction: (-1.3, -1)
			//right corner optimal direction: ( 1.3, -1)

			Console.WriteLine("Starting check for top border collision");
			// -- CHECKING FOR HIT ON TOP BORDER OF PADDLE; INCLUDING CORNERS
			float ballBottomStartY = b.Location.Y + b.Size.Height;
			float maxBallMovementOnXAxis = Math.Abs((newPt.X) - (b.Location.X));

			//top paddle line
			float topRectY = rect.Y;

			//startPoint.X + (ballAngle * stepInXDirection) = topY
			/* See how many steps the ball have to take on x-axis before the bottom of the ball will hit top of paddle */
			double stepInXDirection = Math.Abs((topRectY - ballBottomStartY) / (ballGradient));

			/* if the ball moves that long, then there is collision */
			if (stepInXDirection < maxBallMovementOnXAxis)
			{
				Console.WriteLine("HIT TOP STUFF");

				double hitSpot = (b.Location.X + b.Size.Height / 2 + stepInXDirection) - rect.X;

				// left corner
				if (hitSpot < cornerLength && cornerDeflection)
				{
					//Here we should change the direction
					if (b.IsGoingDown)
					{
						b.InvertVertical();
					}

					if (b.IsGoingRight)
					{
						b.InvertHorizontal();
					}
				}
				else if (cornerDeflection && hitSpot > rect.Width - cornerLength)
				{
					if (b.IsGoingDown)
					{
						b.InvertVertical();
					}

					if (b.IsGoingLeft)
					{
						b.InvertHorizontal();
					}
				}
				else
				{
					b.InvertVertical();
				}

				return;
			}

			/* Checking for hit on bottom side of rect. Including corners
			 * 
			 * Using ballGradient from above
			 */
			float ballTopY = b.Location.Y;

			//bottom rect line
			var bottomRectY = rect.Y + rect.Height;

			stepInXDirection = (bottomRectY - ballTopY) / (ballGradient);

			if (stepInXDirection < maxBallMovementOnXAxis)
			{

				double hitSpot = (b.Location.X + b.Size.Height / 2 + stepInXDirection) - rect.X;

				// left corner
				if (cornerDeflection && hitSpot < cornerLength)
				{
					//Here we should change the direction
					if (b.IsGoingUp)
					{
						b.InvertVertical();
					}

					if (b.IsGoingRight)
					{
						b.InvertHorizontal();
					}
				}
				else if (cornerDeflection && hitSpot > rect.Width - cornerLength)
				{
					if (b.IsGoingUp)
					{
						b.InvertVertical();
					}

					if (b.IsGoingLeft)
					{
						b.InvertHorizontal();
					}
				}
				else
				{
					b.InvertVertical();
				}

				return;
			}

			//-- CHECKING FOR HIT ON RIGHT SIDE BORDER INLCUDE TOP CORNER
			// First the ball variables
			float ballTotalStepInX = Math.Abs(newPt.X - b.Location.X);
			float stepInXUntilHitBorder = Math.Abs((rect.X + rect.Width) - b.Location.X);

			//check if ball even crosses the border
			if (stepInXUntilHitBorder < ballTotalStepInX)
			{
				//check if ball crosses the border in the interval it's defined
				float rightBorderYStart = rect.Y;
				float rightBorderYStop = rect.Y + rect.Height;

				//we do this by comparing the y value of the ball when it crosses the border
				double ballYWhenHit = (b.Location.Y + b.Size.Height / 2) + ballGradient * stepInXUntilHitBorder;

				if (ballYWhenHit > rightBorderYStart && ballYWhenHit < rightBorderYStop)
				{
					double relativeYWhenHit = ballYWhenHit - rect.Y;
					if ((paddleSideHitEqualsCorner && cornerDeflection))
					{
						b.InvertBoth();
					}
					else if (cornerDeflection && relativeYWhenHit < rect.Height / 5)
					{
						b.InvertBoth();
					}
					else
					{
						b.InvertHorizontal();
					}

					return;
				}
			}

			//-- CHECKING FOR HIT ON LEFT SIDE BORDER INCLUDE TOP CORNER
			//first the ball variables:
			stepInXUntilHitBorder = rect.X - (b.Location.X + b.Size.Height);
			if (stepInXUntilHitBorder > 0)
			{
				//we need to inlcude diameter, since we're checking balls right border
				//check if ball even crosses the border
				if (stepInXUntilHitBorder < ballTotalStepInX)
				{
					//check if ball crosses the border in the defined interval
					float leftYStart = rect.Y;
					float leftYStop = rect.Y + rect.Height;

					//we do this by comparing the y value of the ball when it crosses the border
					double ballYWhenHitBorder = (b.Location.Y + b.Size.Height / 2) + ballGradient * stepInXUntilHitBorder;

					if (ballYWhenHitBorder > leftYStart && ballYWhenHitBorder < leftYStop)
					{
						double relativeYWhenHit = ballYWhenHitBorder - rect.Y;
						if (paddleSideHitEqualsCorner && cornerDeflection)
						{
							b.InvertBoth();
						}
						else if (cornerDeflection && relativeYWhenHit < rect.Height / 5)
						{
							b.InvertBoth();
						}
						else
						{
							b.InvertHorizontal();
						}
						return;
					}
				}
			}
		}

		#region " getters/setters "
		public void StartGame()
		{
			state = GameState.GameRunning;
		}

		public void EndGame()
		{
			state = GameState.GameOver;
		}

		public void Pause()
		{
			paused = true;
		}

		public void Resume()
		{
			paused = false;
		}

		public GameState State
		{
			get
			{
				return state;
			}
		}

		public List<Ball> Balls
		{
			get
			{
				return currentLevel.Balls;
			}
		}

		public List<IBlock> Blocks
		{
			get
			{
				return currentLevel.Blocks;
			}
		}

		public List<Paddle> Paddles
		{
			get
			{
				return paddles;
			}
			set
			{
				paddles = value;
			}
		}

		public Size Bounds
		{
			get
			{
				return bounds;
			}
			set
			{
				bounds = value;
			}
		}

		public bool Paused
		{
			get
			{
				return paused;
			}
			set
			{
				paused = value;
			}
		}
		#endregion

		#region " IDisposable Support "
		// IDisposable
		protected virtual void Dispose(bool disposing)
		{
			if (!disposedValue)
			{
				if (disposing)
				{
					// TODO: free other state (managed objects).
					if ((sound != null))
						sound.Dispose();

					paddles = null;
					sound = null;

					if (currentLevel != null)
						currentLevel.Dispose();

				}

			}

			// TODO: free your own state (unmanaged objects).
			// TODO: set large fields to null.
			disposedValue = true;
		}

		// This code added by Visual Basic to correctly implement the disposable pattern.
		public void Dispose()
		{
			// Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
			Dispose(true);
			GC.SuppressFinalize(this);
		}
		#endregion

		private void AddPaddle(PaddleControl paddleControl, Color c)
		{
			paddles.Add(new Paddle(paddleControl, new PointF(300, 500), new SizeF(Paddle.DefaultWidth, Paddle.DefaultHeight), c));
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="p"></param>
		public void LoadLevel(string p)
		{
			/* NOTE THIS SHOULD NOT BE CALLABLE PUBLICLY! */
			this.currentLevel = breakoutsharp.utility.LevelLoader.LoadLevelFromFile(p);
		}

		/// <summary>
		/// Set the number of players/paddles in the game
		/// </summary>
		/// <param name="p">p: the number of paddles 1-4</param>
		public void SetNumberOfPlayers(int p)
		{
			if (p > 4)
			{
				throw new ArgumentOutOfRangeException("p");
			}

			/* clearing previous paddles */
			paddles.Clear();

			/* Adding paddles */
			for (int i = 0; i < p; i++)
			{
				AddPaddle(Settings.defaultPaddleControls[i], Settings.defaultPaddleColours[i]);
			}

		}
	}
}
